<?
/**
 * GENERAL IDEA:
 * One approach: Deviation database with dynamic expansion
 * 1. Define "correct" artistname as the one used at WS (last.fm, musicbrainz.org etc)
 * 2. Associate deviations (alternative spelling) with correct artistname
 * 3. artistName from WS/user is sent to spellChecker
 * 4. spellChecker; (if result on one of subpoints => break => goto 5.)
 * ("check" may be hash with direct inquiry -> increase performance)
 * 4.1 check artistName against "correct" table
 * 4.2 check artistName against deviation table (joined with artistName)
 * 4.3 Perform [naive search] (if true -> added to deviation table => 4.4)
 * 4.4 try last.fm query (external) and cache if result
 * 4.5 we have no idea or the artistName out of domain (not an artist)
 * 
 * [naive search] (very lightweight, stupid, reasoning?) for adding new deviation:
 *  1. Remove all non-english characters (regex /[a-zA-Z]/)
 *  2. find the longest substring of the english character string
 *  3. SQL-query with LIKE %longSubstring% on all deviations and correct artistnames
 *  (4. compute probability for match) NO concrete approach yet.
 *  5. "Trust" the strategy, selecting the first result and 3. and add the deviation to
 * the associated artistname. (MAY CAUSE PROBLEMS! Could integrate last.fm check
 * _before_ add to deviation table)
 * 
 * Database requirements:
 *  - tables; artists, artistvariants
 * 
 */
require_once("Database.php");
require_once("Info.php");
require_once("Lastfm.php");


class ArtistLexica
{
	private $artistNames = null;
	private $artistVariants = null;
	
	public function __construct() {
		define("ARTIST_TABLE", "artists");
		define("VARIANT_TABLE", "artistvariants");
	}
	
	
	/**
	 * Get the correct spelled artist name according to our defined spelling in ARTIST_TABLE
	 * 
	 * tries different strategies and as a final approach; fetch and cache directly from last.fm.
	 * 
	 * @param string $artistName
	 * @param boolean $fetchExternally (last approach - fetch "blind" from external sources)
	 * @return array
	 */
	public function getArtistName($artistName, $fetchExternally = true) {
		if($artistName === null) {
			return null;
		}
		
		Info::add("Trying to fetch a NL spelled artist, " . $artistName);
		
		//try fetch directly from hash (correct spelling)
		$artist = $this->getArtistFromHash($artistName);
		if($artist !== false) {
			//Correct spelling -> return the artist
			return $artist;
		}
		
		//Try next strategy -> search variant spellings
		$artist = $this->getArtistFromVariants($artistName);
		if($artist !== false) {
			//Found variant! Return true artist.
			return $artist;
		}
		
		//Try "similarity-search" => NOT BULLETPROOF!
		$artist = $this->findArtistName($artistName);
		if($artist !== false) {
			//Needs more "safe" validation
			return $this->artistNames[$artist[0]['artistName']];
		}
		
		if($fetchExternally == true) {
			//Try fetching directly from last.fm
			$artist = $this->fetchArtistFromLastfm($artistName);
			if($artist !== false) {
				//Found the artist on Last.fm => actually the spelling was correct but we did not know it.
				Info::add("Artist found directly from last.fm and is cached locally; " . $artistName);
				return $this->artistNames[$artistName];
			}
		}
		
		Info::add("Can not determine the artist by the current strategy; " . $artistName);
		return false;
	}	
	
	
	/**
	 * Tries to find the correct artist named assuming it is not spelled correctly
	 * Checks how similar the input string is to the artist names stored locally (db/cache)
	 * If it is sufficient similarity then the artist is returned.
	 * The similarity algorithm is based on Levenshtein-distance (edit-distance)
	 * Condition for correct spelled is l-distance less than 3
	 * 
	 * KNOWN ISSUES; 
	 *  - levenshtein() counts æøå as 2 characters - giving very little probability for success when artistname is spelled with one of æøå
	 *     - Could replace æøå with a native english character, assuming levenshtein algorithm is used => makes no difference
	 *  - Could relax more on conditions (default param values)
	 *  - Could use similar_text() instead, slower but generally more accurate - though not necessarily in this context?
	 * 
	 * @param string $artistName
	 * @param int $maxLDist (represent the maximum allowed L-distance)
	 */
	private function findArtistName($artistName, $maxLDist = 2, $maxWrongPercentage = 50) {
		Info::add("Trying to check against spelling errors");
		$artistNameOrig = $artistName;
		//strip away known, non-significant characters
		$artistName = stripcslashes($artistName);
		$unsignChar = array(0,1,2,3,4,5,6,7,8,9,"%","+");
		$artistName = str_ireplace($unsignChar, "", $artistName);
		Info::add("Checking ; " . $artistName);
		//retrieve the correct artistNames (our lexica)
		$artistNamesLexical = $this->fetchArtists();
		
		$correctArtists = false;
		//find the Levenshtein-distance and save the one with min L-dist
		foreach ($artistNamesLexical as $artistNameLex => $artistNameLexical) {
			$lDistance = levenshtein($artistName, $artistNameLex);
			$percentageWrong = round(($lDistance / strlen($artistNameLex))*100, 0);
			
			//Info::add("Levenshtein dist; " . $artistName . " and " . $artistNameLex . " (orig: " . $artistNameOrig . " ) is " . $lDistance);
			//the artist in question is the current most similar spelled artist
			if ($maxLDist >= $lDistance and $percentageWrong <= $maxWrongPercentage) {
				$artistNameLexical['editDistance'] = $lDistance;
				$artistNameLexical['percentageWrongSpelled'] = $percentageWrong;
				$correctArtists[] = $artistNameLexical;
			}
		}
		
		//Sort the matches by edit-distance
		foreach($correctArtists as $sortarray)
		{
			$column[] = $sortarray['editDistance'];
			array_multisort($column, SORT_ASC, $correctArtists);
		}
		
		Info::add("Found possible matches; <pre> " . var_export($correctArtists, true));
		
		return $correctArtists;
	}
	
	/**
	 * Fetches correct artists from database and saves the result in 
	 * $this->artistNames as a hash with artistName as key.
	 * 
	 * @return void
	 */
	private function fetchArtists($forceUpdate = false) {
		//The data has already been fetched
		if(($this->artistNames !== null) and ($forceUpdate == false)) {
			return $this->artistNames;
		}
		
		Info::add("SpellChecker::fetchArtists is forced/needs to update.");
		$db = Database::getInstance();
		
		$query = "SELECT * FROM " . ARTIST_TABLE;
		$result = $db->query($query);
		
		if($result == false) {
			throw new Exception("Query failed; fetchArtists.", E_USER_ERROR);
			return;
		}
		
		while ($artist = mysql_fetch_assoc($result)) {
			$this->artistNames[$artist['artistName']] = $artist;
		}
		return $this->artistNames;
	}
	
	/**
	 * fetch the artist variants and put them in a hashstack with variant spelling as key
	 * ASSUMING; no variants spelled identically (throughout the whole variant table)
	 *
	 * @return array
	 */
	private function fetchVariants() {
		//The data has already been fetched
		if($this->artistVariants !== null) {
			
			return $this->artistVariants;
		}
		$db = Database::getInstance();
		
		$query = "SELECT * FROM 
			" . VARIANT_TABLE . " JOIN " . ARTIST_TABLE . "
				ON " . VARIANT_TABLE . ".correctartist = " . ARTIST_TABLE . ".id"; 
		$result = $db->query($query);
		
		if($result == false) {
			throw new Exception("Query failed.", E_ERROR);
			return;
		}
		
		while ($artist = mysql_fetch_assoc($result)) {
			$this->artistVariants[$artist['variant']] = $artist;
		}
		
		Info::add("Artist variants are fetched from db.");
		return $this->artistVariants;
	}
	
	/**
	 * Adds an artistname to the lexica (db) as correct spelled
	 *
	 * @param (dbsafe)string $artistName
	 */
	public function addArtistName($artistName) {
		$query = "INSERT INTO `musiceye`.`artists` (`id` ,`artistName` ,`cached` ,`cachedat`)
				VALUES (
					NULL , '" . $artistName . "', 'true', NOW()
				)";
		$db = Database::getInstance();
		$result = $db->query($query);
		
		if($result == false) {
			throw new Exception("Query failed.", E_ERROR);
			return;
		}
		Info::add("Artist is added to artist database; " . $artistName);
	}
	
	private function fetchArtistFromLastfm($artistName) {
		$result = Lastfm::cacheSimilarArtists($artistName);
		if($result == false) {
			return false;
		}
		Info::add("Artist found at last.fm; " . $artistName);
		
		//add the artist to DB and update local hash
		$this->addArtistName($artistName);
		$this->fetchArtists(true);
		
		return $artist;
	}
	
	/**
	 * Tries to fetch directly from the hashstack with correct spelling
	 *
	 * @param string $artistName
	 * @return array
	 */
	private function getArtistFromHash($artistName) {
		$this->fetchArtists();
		$artist = null;
		
		if(key_exists($artistName, $this->artistNames)) {
			$artist[$artistName] = $this->artistNames[$artistName];
			Info::add("Artist found directly in local cache with correct spelling; " . $artistName);
			return $artist;
		}
		
		Info::add("Artist NOT found directly in local cache with correct spelling; " . $artistName);		
		//none found
		return false;		
	}
	
	/**
	 * Tries to fetch directly from the hashstack with the variant spellings
	 *
	 * @param string $artistName
	 * @return array
	 */
	private function getArtistFromVariants($artistName) {
		$this->fetchVariants();
		$artist = null;
		echo "<pre>";

		if(key_exists($artistName, $this->artistVariants)) {
			//variant exists -> return the correct spelled artist information
			$correctName = $this->artistVariants[$artistName]['artistName'];
			$artist[$correctName] = $this->artistNames[$correctName];
			Info::add("Artist found as a local variant spelling; " . $artistName);
			return $artist;
		}
		Info::add("Artist NOT found in local variants. " . $artistName);
		//none found
		return false;
	}
	
	
	
}